﻿using System;
using System.Collections.Generic;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Text;
using System.Windows.Forms;
using BGShell.Components;
using BGShell.PowerShell.DataTips;
using BGShell.PowerShell.Host;
using Microsoft.PowerShell.Commands;

namespace BGShell.PowerShell.Runspaces
{
    internal sealed class RunspaceProxyService : MarshalByRefObject, IRunspaceProxy
    {
        private const string GetCommand = "Microsoft.PowerShell.Core\\Get-Command";

        private static readonly Command GetHistory = new Command("Get-History")
        {
            Parameters = { new CommandParameter("Count", Int16.MaxValue) }
        };

        private const string GetItem = "Microsoft.PowerShell.Management\\Get-Item";

        private static readonly Command OutDefault =
            new Command("Microsoft.PowerShell.Utility\\Out-Default")
            {
                MergeUnclaimedPreviousCommandResults =
                    PipelineResultTypes.Output |
                    PipelineResultTypes.Error,
            };

        private static readonly Command OutString =
            new Command("Microsoft.PowerShell.Utility\\Out-String");

        private static readonly Command Prompt =
            new Command("Prompt");

        private const string TabExpansion = "TabExpansion";

        private const string WriteHost = "Microsoft.PowerShell.Utility\\Write-Host";

        private Runspace _runspace;
        private BGShellPipelineExecutor _exec;
        private Stack<BGShellPipelineExecutor> _nestedExecs;

        private IRunspaceHostUI _runspaceHostUi;

        private RunspacePSHost _runspaceHost;
        private RunspaceHostInfo _runspaceHostInfo;

        private Int32 _exitCode;
        private KeyMappingDictionary _keyMappings;

        internal RunspaceProxyService(RunspaceHostInfo hostInfo, IRunspaceHostUI hostUi, MarshalByRefObject hostPrivateObject)
        {
            _runspaceHostUi = hostUi;
            _runspaceHostInfo = hostInfo;

            _runspaceHost = new RunspacePSHost(this, hostInfo, hostUi, hostPrivateObject);

            if (hostInfo.IsBackground)
            {
                _keyMappings = new KeyMappingDictionary(hostUi);
            }
        }

        public void Dispose()
        {
            _runspaceHost = null;

            if (_nestedExecs != null)
            {
                foreach (var e in _nestedExecs)
                {
                    e.Dispose();
                }

                _nestedExecs.Clear();
            }

            if (_exec != null)
            {
                _exec.Dispose();
                _exec = null;
            }

            if (_runspace != null)
            {
                _runspace.StateChanged -= OnRunspaceStateChanged;
                _runspace.Dispose();
                _runspace = null;
            }
        }

        /// <remarks>
        /// This is being called on the pipeline execution thread.
        /// </remarks>
        internal void SetShouldExit(int exitCode)
        {
            _exitCode = exitCode;

            if ((_runspace != null) && (_runspace.RunspaceStateInfo.State == RunspaceState.Opened))
            {
                _runspace.CloseAsync();
            }
        }

        /// <remarks>
        /// This is being called on the pipeline execution thread.
        /// </remarks>
        internal void EnterNestedPrompt()
        {
            if (_nestedExecs == null)
            {
                _nestedExecs = new Stack<BGShellPipelineExecutor>();
            }

            var exec = new BGShellPipelineExecutor(_runspace, true);
            _nestedExecs.Push(exec);

            exec.Enqueue(PromptPipeline);
            exec.Run();
        }

        /// <remarks>
        /// This is being called on the pipeline execution thread.
        /// </remarks>
        internal void ExitNestedPrompt()
        {
            if ((_nestedExecs != null) && (_nestedExecs.Count > 0))
            {
                _nestedExecs.Pop().Exit();
            }
        }

        BGShellPipelineProxy IRunspaceProxy.BeginGetHistory(Action<String[]> callback)
        {
            return EnqueueAndReturnProxy(new BGShellPipeline(OnGotHistory)
            {
                UserState = callback,
                Commands = { GetHistory },
            });
        }

        BGShellPipelineProxy IRunspaceProxy.BeginGetTabExpansions(Action<String[]> callback, String line, String lastWord)
        {
            return EnqueueAndReturnProxy(new BGShellPipeline(OnGotTabExpansions)
            {
                UserState = callback,
                Commands = 
                { 
                    new Command(TabExpansion)
                    {
                        Parameters = 
                        {
                            new CommandParameter("Line", line),
                            new CommandParameter("LastWord", lastWord),
                        },
                    },
                },
            });
        }

        BGShellPipelineProxy IRunspaceProxy.BeginGetDataTip(Action<DataTipInfo> callback, DataTipInfo request)
        {
            if (request.Type == DataTipType.Variable)
            {
                callback(new DataTipInfo(request, new VariableDataTip(_runspace, request.Text)));
            }
            else
            {
                BGShellPipeline pipe = new BGShellPipeline(OnGotDataTip)
                {
                    UserState = new DataTipPipelineState(callback, request),
                };

                if (request.Type == DataTipType.Type)
                {
                    pipe.Commands.Add(new Command('[' + request.Text.Replace("`", "``") + ']', true));
                }
                else if (request.Type == DataTipType.Command)
                {
                    pipe.Commands.Add(new Command(GetCommand)
                    {
                        Parameters = { new CommandParameter("Name", WildcardPattern.Escape(request.Text)) }
                    });
                }

                return EnqueueAndReturnProxy(pipe);
            }

            return null;
        }

        BGShellPipelineProxy IRunspaceProxy.InvokeOutDefault(String command)
        {
            if (command != null)
            {
                command = command.Trim();
            }

            if (string.IsNullOrEmpty(command))
            {
                InvokePromptFunctionNoErrorReporting(null);
                return null;
            }
            else
            {
                var pipe = new BGShellPipeline(command, true)
                {
                    Commands = { OutDefault },
                };

                return EnqueueAndReturnProxy(pipe, OnOutDefaultPipelineDisposed);
            }
        }

        BGShellPipelineProxy IRunspaceProxy.InvokeHookKey(Keys key)
        {
            if (_keyMappings != null)
            {
                var pipeline = _keyMappings.GetPipeline(key);

                if (pipeline != null)
                {
                    return EnqueueAndReturnProxy(pipeline);
                }
            }

            return null;
        }

        int IRunspaceProxy.GetExitCode()
        {
            return _exitCode;
        }

        void IRunspaceProxy.OpenAsync()
        {
            if (_runspace == null)
            {
                var session = DefaultProfile.CreateSessionState();

                _runspace = RunspaceFactory.CreateRunspace(_runspaceHost, session);
                _runspace.StateChanged += OnRunspaceStateChanged;
                _runspace.OpenAsync();
            }
        }

        void IRunspaceProxy.CloseAsync()
        {
            SetShouldExit(0);
        }

        private BGShellPipeline PromptPipeline
        {
            get
            {
                return new BGShellPipeline(OnGotPrompt)
                {
                    UserState = _runspaceHostUi,
                    Commands = { Prompt, OutString },
                };
            }
        }

        private BGShellPipelineExecutor CurrentExecutor
        {
            get
            {
                if ((_nestedExecs != null) && (_nestedExecs.Count > 0))
                {
                    return _nestedExecs.Peek();
                }

                return _exec;
            }
        }

        private void Enqueue(BGShellPipeline pipe)
        {
            CurrentExecutor.Enqueue(pipe);
        }

        private BGShellPipelineProxy EnqueueAndReturnProxy(BGShellPipeline pipe)
        {
            return EnqueueAndReturnProxy(pipe, null);
        }

        private BGShellPipelineProxy EnqueueAndReturnProxy(BGShellPipeline pipe, EventHandler onDisposed)
        {
            if (pipe != null)
            {
                if (onDisposed != null)
                {
                    pipe.PipelineDisposed += onDisposed;
                }

                CurrentExecutor.Enqueue(pipe);
                return new BGShellPipelineProxy(CurrentExecutor, pipe);
            }

            return null;
        }

        private void InvokePromptFunctionNoErrorReporting(BGShellPipeline prevPipe)
        {
            if (_runspace.RunspaceStateInfo.State == RunspaceState.Opened)
            {
                Enqueue(PromptPipeline);
            }
        }

        private void InvokePromptFunction(BGShellPipeline prevPipe)
        {
            if ((prevPipe != null) && (prevPipe.FailureReason != null))
            {
                var errorRecordPSObject = new PSObject(prevPipe.FailureReason)
                {
                    Properties = { new PSNoteProperty("writeErrorStream", true) }
                };

                Enqueue(new BGShellPipeline(InvokePromptFunctionNoErrorReporting)
                {
                    Commands = { OutDefault },
                    Input = { errorRecordPSObject },
                });
            }
            else
            {
                InvokePromptFunctionNoErrorReporting(prevPipe);
            }
        }

        // Pipeline callbacks

        private static void OnGotDataTip(BGShellPipeline pipe)
        {
            var state = pipe.UserState as DataTipPipelineState;

            if (state == null)
            {
                return;
            }

            DataTipItem tip = null;
            String error = null;

            if (state.Info.Type == DataTipType.Command)
            {
                var cmdInfo = GetPipelineOutput<CommandInfo>(pipe, out error);

                if (cmdInfo != null)
                {
                    tip = new CommandInfoDataTip(cmdInfo);
                }
            }
            else if (state.Info.Type == DataTipType.Type)
            {
                var type = GetPipelineOutput<Type>(pipe, out error);

                if (type != null)
                {
                    tip = new TypeLiteralDataTip(type);
                }
            }

            if (!String.IsNullOrEmpty(error))
            {
                tip = new ErrorDataTip(error);
            }

            if (tip != null)
            {
                state.Callback(new DataTipInfo(state.Info, tip));
            }
        }

        private static void OnGotHistory(BGShellPipeline pipe)
        {
            var callback = pipe.UserState as Action<String[]>;

            if (callback != null)
            {
                var strings = GetPipelineOutputAsStrings(pipe, HistoryInfoToString);
                var results = new List<String>(strings.Length);

                for (int i = 0; i < strings.Length; i++)
                {
                    if ((i == 0) || !(String.Equals(strings[i - 1], strings[i], StringComparison.Ordinal)))
                    {
                        results.Add(strings[i]);
                    }
                }

                callback(results.ToArray());
            }
        }

        private static void OnGotPrompt(BGShellPipeline pipe)
        {
            var prompt = "PS> ";
            var result = new StringBuilder();

            foreach (var psobject in pipe.Output)
            {
                result.Append(psobject);
            }

            if (result.Length > 0)
            {
                if (result[result.Length - 1] == '\n')
                {
                    --result.Length;

                    if ((result.Length > 0) && (result[result.Length - 1] == '\r'))
                    {
                        --result.Length;
                    }
                }

                prompt = result.ToString();
            }

            var ui = pipe.UserState as IRunspaceHostUI;
            
            if (ui != null)
            {
                ui.WritePrompt(prompt);
            }
        }

        private static void OnGotTabExpansions(BGShellPipeline pipe)
        {
            var callback = pipe.UserState as Action<String[]>;

            if (callback != null)
            {
                callback(GetPipelineOutputAsStrings(pipe));
            }
        }

        // Runspace and pipeline events
        
        private void OnRunspaceStateChanged(object sender, RunspaceStateEventArgs e)
        {
            switch (e.RunspaceStateInfo.State)
            {
                case RunspaceState.Closed:
                    _runspaceHostUi.SetState(RunspaceHostState.Closed);
                    Dispose();
                    return;

                case RunspaceState.Opened:
                    InitRunspace();
                    return;
            }

            _runspaceHostUi.SetState(RunspaceHostState.Unknown);
        }

        private void OnOutDefaultPipelineDisposed(object sender, EventArgs e)
        {
            var pipe = (sender as BGShellPipeline);

            if (pipe != null)
            {
                if (pipe.UserState != BGShellPipelineExecutor.IsExitPipeline)
                {
                    InvokePromptFunction(pipe);
                }
            }
        }

        private void InitRunspace()
        {
            var profiles = new UserProfiles(_runspaceHostInfo.Name);

            _runspace.SessionStateProxy.SetVariable("Profile", profiles.UserHostProfile.FullName);

            if (_keyMappings != null)
            {
                _runspace.SessionStateProxy.SetVariable("KeyMapping", _keyMappings);
            }

            _exec = new BGShellPipelineExecutor(_runspace, false);

            foreach (var command in DefaultProfile.GetInitializationPipelines())
            {
                Enqueue(new BGShellPipeline(command));
            }

            foreach (var profile in profiles)
            {
                Enqueue(new BGShellPipeline(new Command(profile.FullName, false, true)));
            }

            _runspaceHostUi.SetState(RunspaceHostState.Opened);
            InvokePromptFunction(null);
        }
            
        // Pipeline output processing

        private static T GetPipelineOutput<T>(BGShellPipeline pipe)
        {
            string error;
            return GetPipelineOutput<T>(pipe, out error);
        }

        private static T GetPipelineOutput<T>(BGShellPipeline pipe, out string error)
        {
            if (pipe.FailureReason != null)
            {
                error = pipe.FailureReason.Exception.Message;
                return default(T);
            }

            if (pipe.Errors.Count > 0)
            {
                var message = new StringBuilder();

                foreach (Object err in pipe.Errors)
                {
                    message.AppendLine(err.ToString());
                }

                error = message.ToString().Trim();
                return default(T);
            }

            foreach (var obj in pipe.Output)
            {
                if (obj.BaseObject is T)
                {
                    error = null;
                    return (T)(obj.BaseObject);
                }
            }

            error = null;
            return default(T);
        }

        private static string[] GetPipelineOutputAsStrings(BGShellPipeline pipeline)
        {
            return GetPipelineOutputAsStrings(pipeline, psobj =>
            {
                if (psobj != null)
                {
                    return psobj.ToString();
                }

                return String.Empty;
            });
        }

        private static string[] GetPipelineOutputAsStrings(BGShellPipeline pipeline, Converter<PSObject, String> converter)
        {
            var result = new List<String>();

            foreach (var psobject in pipeline.Output)
            {
                var str = converter(psobject);

                if (str != null)
                {
                    result.Add(str);
                }
            }

            return result.ToArray();
        }

        private static string HistoryInfoToString(PSObject psobject)
        {
            var info = psobject.BaseObject as HistoryInfo;

            if (info != null)
            {
                string cmd = info.CommandLine;

                if (cmd != null)
                {
                    cmd = cmd.Trim();

                    if (!string.IsNullOrEmpty(cmd))
                    {
                        return cmd;
                    }
                }
            }

            return null;
        }

        private sealed class DataTipPipelineState
        {
            public DataTipPipelineState(Action<DataTipInfo> callback, DataTipInfo info)
            {
                Info = info;
                Callback = callback;
            }

            public readonly DataTipInfo Info;
            public readonly Action<DataTipInfo> Callback;
        }
    }
}